Deployment
Léigh Dom deploys like every other user-facing ABAIR app: CI builds an image on docker-build, pushes it to the internal registry, and SSHes into services-user to restart the stack. Merging to main is the whole release process.
| Repository | phonlab-tcd/leigh-dom |
| Image | 10.0.0.12:5000/leigh-dom:main |
| Host | services-user (10.0.0.18), port 3015 → container 3000 |
| Compose stack | /opt/apps/leigh-dom/ |
| Public URL | https://leighdom.abair.ie |
| Stage | Trigger | What happens |
|---|---|---|
| CI | PR to main | npm ci, typecheck, lint, tests with coverage, and a production next build. |
| CD — build | Push to main | Builds the image on the self-hosted runner and pushes it to the internal registry. |
| CD — deploy | After the build job | ssh deploy@10.0.0.18 "cd /opt/apps/leigh-dom && docker compose pull && docker compose up -d". |
There is no registry login step: the github-runner user on docker-build is pre-authenticated by setup-docker-build.yml. Both jobs run in the prod GitHub environment, which holds the build-time variables.
Configuration
NEXT_PUBLIC_* values referenced from client components are inlined into the browser bundle by next build — setting them only in the compose environment: block leaves the browser with undefined. The sign-in link, account menu and 401 recovery all run client-side, so their config has to arrive as build args.
| Variable | Set where | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Build arg | From vars.* in the prod environment |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Build arg | Public by design |
NEXT_PUBLIC_AUTH_BASE_URL | Build arg | https://auth.abair.ie |
NEXT_PUBLIC_SITE_URL | Both | Read again server-side by the middleware and /auth/callback, so the two must match |
PUBLIC_ORIGIN | Compose | https://leighdom.abair.ie |
RETENTION_DAYS | Compose | 30 |
ADMIN_API_TOKEN | .env (Vaultwarden) | Bearer token for the admin API CÚL uses. At least 32 characters; unset admits nobody |
Everything else keeps the defaults in lib/config.ts.
PUBLIC_ORIGIN is the one value that must not be left at its default. It is sent as Origin and Referer to synthesis.abair.ie, which rejects an empty or mismatched value — its default of http://localhost:3000 fails every synthesis request in production while the app looks perfectly healthy.
.env
One value Léigh Dom reads at run time is secret: ADMIN_API_TOKEN, shared with CÚL as LEIGHDOM_API_TOKEN. So it is in user_apps_with_env, and /opt/apps/leigh-dom/.env is written from the Vaultwarden note leigh-dom .env — the convention abair_website, geabaire-api, giob-geab and mao-teacher-portal follow. The compose file reads it with env_file, so the note has to exist before the next make deploy-services-user-apps, and the file before the next CD run.
Persistence
The stack declares a named data volume rather than a bind mount, so there is nothing to rsync in on first deploy: Docker creates it on the first up -d and the entrypoint creates the subtrees. pull && up -d leaves it intact; only down -v destroys it.
A named volume is not picked up by anything backing up /opt/apps. Treat the contents as regenerable — a lost volume costs users their document history, not the system its configuration.
nginx
networking/nginx-config/leighdom.abair.ie needs two things the standard template does not have:
client_max_body_size 64M. The app's cap is 50 MiB (MAX_DOC_BYTES); nginx's 1 MB default would reject every upload with a bare 413 first. 64 MiB sits above the app's cap with room for the multipart envelope, so an oversized file gets the app's error instead.proxy_http_version 1.1withConnectioncleared. The progress stream is Server-Sent Events, and nginx defaults to HTTP/1.0 upstream, which drops the keep-alive it relies on./api/admin/stats,/api/admin/users/and/api/admin/documents/answer 404. The admin API is for CÚL over the private network, and it can delete documents; from the internet it does not exist, and the token is the second lock rather than the only one.
No buffering or timeout overrides are needed — the app sends X-Accel-Buffering: no and its own 15-second keep-alive.
Checking what is deployed
/api/health answers more than liveness, and 503 when the database or a statvfs call fails:
curl -s https://leighdom.abair.ie/api/health | jq
{
"ok": true,
"db": "ok",
"diskFreeBytes": 8123456789,
"runner": { "running": true, "activeJobs": 0 },
"video": { "available": true },
"queue": { "oldestQueuedAgeS": 0 }
}
runner and queue separate "healthy and idle" from "healthy-looking but backing up": oldestQueuedAgeS climbing while runner.running is false means the worker died without taking the process down. video.available: false means ffmpeg is missing — the client hides the export rather than offering a button that always fails, so check the probe, not the UI.
On the host:
ssh services-user-vm "cd /opt/apps/leigh-dom && docker compose ps && docker compose logs --tail 50"
Adding it to a fresh estate
ansible/hetzner-main/services-user-apps/leigh-dom/docker-compose.yml— the stack.leigh-domin theuser_appslist insetup-services-user.yml.networking/nginx-config/leighdom.abair.ie— the vhost.- Deploy:
make deploy-services-user-appsmake update-webserver
- A Cloudflare DNS record for
leighdom. The wildcard*.abair.iecertificate already covers it. - Apply
supabase/0001_lei_uploads.sqlonce against the shared auth project, then0002–0003(tiers, deletions). Idempotent, and the app degrades gracefully without them.0004_lei_admin.sqladds thelei_adminrole CÚL's Léigh Dom page checks; it must exist before CÚL lists the role in User Management. Its read policies and functions, and0005_lei_admin_tiers.sql, served an earlier dashboard and are unused: CÚL uses its secret key.
No allow-list entry is needed in the auth app: TRUSTED_BASE_DOMAINS = ["abair.ie"] in src/config/authOrigins.js already trusts every https://*.abair.ie origin. See Authentication.
Last updated 2026-10-05